{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a7a017f6",
   "metadata": {},
   "source": [
    "# 一、从零实现一个线性模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f938fa10",
   "metadata": {},
   "source": [
    "# 1. 构造一个数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "c4ad92a7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "import numpy as np\n",
    "import torch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 83,
   "id": "9b2cafef",
   "metadata": {},
   "outputs": [],
   "source": [
    "#手工构造一个数据集\n",
    "# y = Wx+b+e\n",
    "\n",
    "def synthetic_data(w, b, num_examples):\n",
    "    \"\"\"\n",
    "    w: a list of weights\n",
    "    b: bias\n",
    "    num_examples: namely\n",
    "    \"\"\"\n",
    "    X = np.random.normal(0, 1,(num_examples, len(w)))  # X服从0~1的正态分布\n",
    "    y = np.matmul(X, w) + b\n",
    "    y += np.random.normal(0, 0.01, y.shape)  # 添加噪音\n",
    "    return X, y.reshape((-1, 1))\n",
    "\n",
    "w_true = np.array([2, -3.4])\n",
    "b_true = 4.2\n",
    "features, labels = synthetic_data(w_true, b_true, 1000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "0cb16b27",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(1000, 2)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "(array([[-1.89514136, -0.05785525],\n",
       "        [-0.2005648 ,  0.60202221],\n",
       "        [ 1.04071886, -1.62725927],\n",
       "        [ 0.72767983,  1.28517374],\n",
       "        [-0.90566961,  1.29478896]]),\n",
       " array([[ 0.58655753],\n",
       "        [ 1.75160523],\n",
       "        [11.80399827],\n",
       "        [ 1.28593393],\n",
       "        [-2.01440084]]))"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(features.shape)\n",
    "features[:5], labels[:5]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "e71ce51b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.collections.PathCollection at 0x148b453a0>"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "plt.scatter(features[:, 0], labels)\n",
    "plt.scatter(features[:, 1], labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eab7bb2d",
   "metadata": {},
   "source": [
    "# 2.构建批量采样函数、模型、损失函数、优化算法"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "id": "51bf90f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 数据采样函数\n",
    "def data_iter(batch_size, features, labels):\n",
    "    assert len(features) == len(labels), \"not match\"\n",
    "    num_examples = len(labels)\n",
    "    indices = list(range(num_examples))\n",
    "    random.shuffle(indices)\n",
    "    for i in range(0, num_examples, batch_size):  # 今天才知道range函数还有第三个参数可选！！\n",
    "        batch_features = features[i : i + batch_size]\n",
    "        batch_labels = labels[i : i + batch_size]\n",
    "        yield batch_features, batch_labels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "3ba53412",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0, 1, 2, 3] [1, 1, 1, 1]\n",
      "[4, 5, 6, 7] [1, 1, 1, 1]\n",
      "[8, 9, 10, 11] [1, 1, 1, 1]\n"
     ]
    }
   ],
   "source": [
    "for X, y in data_iter(4,list(range(12)),[1]*12):\n",
    "    print(X, y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "c6e94689",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义模型\n",
    "def linreg(X, w, b):\n",
    "    return torch.matmul(X, w) + b"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "3f0617a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义损失函数\n",
    "def squared_loss(y_true, y_pred):\n",
    "    return torch.sum((y_true - y_pred)**2)/2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "f7ad4e19",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义优化算法\n",
    "def sgd(params, lr, batch_size):\n",
    "    \"\"\"\n",
    "    params: a list of params\n",
    "    \"\"\"\n",
    "    with torch.no_grad():  # 设置停止torch的梯度记录\n",
    "        for param in params:\n",
    "            param -= lr * param.grad / batch_size  # 等价于在loss中除以batch_size\n",
    "            param.grad.zero_()  # 重置该参数的梯度，便于下一次计算"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14180de3",
   "metadata": {},
   "source": [
    "# 3.进行训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "id": "c879047d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 1, loss 13399.2265625\n",
      "epoch 11, loss 1865.0733642578125\n",
      "epoch 21, loss 260.1598205566406\n",
      "epoch 31, loss 36.40078353881836\n",
      "epoch 41, loss 5.138577461242676\n",
      "epoch 51, loss 0.76097571849823\n",
      "epoch 61, loss 0.1465410739183426\n",
      "epoch 71, loss 0.060093529522418976\n",
      "epoch 81, loss 0.0479029044508934\n",
      "epoch 91, loss 0.04618138074874878\n"
     ]
    }
   ],
   "source": [
    "lr = 0.01\n",
    "epochs = 100\n",
    "batch_size = 100\n",
    "model = linreg\n",
    "loss_func = squared_loss\n",
    "\n",
    "# 参数初始化\n",
    "w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)\n",
    "b = torch.zeros(1, requires_grad=True)\n",
    "# numpy的数据默认格式为float64，但torch中默认使用float32\n",
    "# 同时，torch模型也不能直接处理numpy数据，需要进行格式转换\n",
    "features = torch.tensor(features, dtype=torch.float32)  \n",
    "labels = torch.tensor(labels, dtype=torch.float32)\n",
    "\n",
    "for i in range(epochs):\n",
    "    for X, y in data_iter(batch_size, features, labels):\n",
    "        y_pred = model(X, w, b)\n",
    "        l = loss_func(y, y_pred)\n",
    "        l.sum().backward()\n",
    "        sgd([w, b], lr, batch_size)\n",
    "    with torch.no_grad():\n",
    "        if i % 10 == 0:\n",
    "            epoch_loss = loss_func(model(features, w, b), labels)\n",
    "            print(f'epoch {i + 1}, loss {float(epoch_loss)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "id": "31b12e90",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "w的估计误差: [-0.0004003  -0.00041475]\n",
      "b的估计误差: -4.100799560369239e-06\n"
     ]
    }
   ],
   "source": [
    "# 由于w带了requires_grad属性，所以要转化成numpy，需要先detach\n",
    "print(f'w的估计误差: {w_true - w.detach().numpy().reshape(w_true.shape)}')\n",
    "print(f'b的估计误差: {b_true - float(b.detach())}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b23f97f6",
   "metadata": {},
   "source": [
    "# 二、利用框架快速实现"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "id": "9b4e96ea",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import torch\n",
    "from torch.utils import data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 87,
   "id": "bb33afe3",
   "metadata": {},
   "outputs": [],
   "source": [
    "features = torch.tensor(features, dtype=torch.float32)  \n",
    "labels = torch.tensor(labels, dtype=torch.float32)\n",
    "\n",
    "def load_array(data_arrays, batch_size, is_train=True):\n",
    "    \"\"\"构造一个PyTorch数据迭代器\"\"\"\n",
    "    dataset = data.TensorDataset(*data_arrays)\n",
    "    return data.DataLoader(dataset, batch_size, shuffle=is_train)\n",
    "\n",
    "batch_size = 10\n",
    "data_iter = load_array((features, labels), batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea1349b7",
   "metadata": {},
   "source": [
    "## 简单提一下这里的`TensorDataset`和`DataLoader`\n",
    "`TensorDataset`这玩意儿，我不是很懂有什么大作用。感觉必须配合`DataLoader`使用，因为`Dataloader`接受的就是Dataset格式的输入。\\\n",
    "另外，`TensorDataset`的输入，最常见的就是`(X, y)`，然后返回的dataset就是把二者打包的结果"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 101,
   "id": "6e27974f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(tensor([[1, 2, 3],\n",
      "        [1, 2, 2],\n",
      "        [3, 2, 1]]), tensor([1, 2, 1]))\n"
     ]
    }
   ],
   "source": [
    "# 看看TensorDataset在干嘛：\n",
    "X = torch.tensor([[1,2,3],[1,2,2],[3,2,1]])  # 样本的特征\n",
    "y = torch.tensor([1,2,1])  # 样本的标签\n",
    "dataset = data.TensorDataset(X, y)\n",
    "print(dataset[:])  # 可以看到，就是把输入的X和y打包了，方便DataLoader直接使用"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 113,
   "id": "0833970a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "([tensor([[1, 2, 3],\n",
       "          [1, 2, 2]]),\n",
       "  tensor([1, 2])],\n",
       " [tensor([[3, 2, 1]]), tensor([1])])"
      ]
     },
     "execution_count": 113,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data_iter = data.DataLoader(dataset, batch_size=2)\n",
    "f = iter(data_iter)\n",
    "next(f),next(f)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 89,
   "id": "06f6da48",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([0.])"
      ]
     },
     "execution_count": 89,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from torch import nn\n",
    "model = nn.Sequential(nn.Linear(2, 1))\n",
    "\n",
    "model[0].weight.data.normal_(0, 0.01)  #  model[0]代表取出模型的第一层，使用.normal_或者.fill_来赋值\n",
    "model[0].bias.data.fill_(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 90,
   "id": "75cb124f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 设置损失函数、优化器\n",
    "loss = nn.MSELoss()\n",
    "trainer = torch.optim.SGD(model.parameters(), lr=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 91,
   "id": "5ce3b7c6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 0, loss 0.6866896152496338\n",
      "epoch 1, loss 0.01537397038191557\n",
      "epoch 2, loss 0.00043629383435472846\n",
      "epoch 3, loss 0.00010447522799950093\n",
      "epoch 4, loss 9.759785461938009e-05\n",
      "epoch 5, loss 9.739570668898523e-05\n",
      "epoch 6, loss 9.748060256242752e-05\n",
      "epoch 7, loss 9.744378621689975e-05\n",
      "epoch 8, loss 9.740984387462959e-05\n",
      "epoch 9, loss 9.75194270722568e-05\n"
     ]
    }
   ],
   "source": [
    "epochs = 10\n",
    "for i in range(epochs):\n",
    "    for X, y in data_iter:\n",
    "        y_pred = model(X)\n",
    "        l = loss(y_pred, y)\n",
    "        trainer.zero_grad()\n",
    "        l.backward()\n",
    "        trainer.step()\n",
    "    epoch_l = loss(model(features), labels)\n",
    "    print(f'epoch {i}, loss {epoch_l}')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
